0%

Java Native Interface (JNI)【译】

引言

查看关于 JNI 相关资料的时候,不巧碰到了这篇文章,通篇读了一下感觉写的很不错,所以拿过来翻译了一下,由于翻译的比较快,有些细节方面的工作可能做的不是很到位,不过后期会进行相关修正,这里先放上来尝尝鲜。
如果你也对 JNI 比较感兴趣,并且打算深入学习,那么本文将会是一个不错的选择。

1.0 JNI 概述

  • 什么是 JNI ?
    JNI 是 Java 与其他语言交互的一个桥梁。
  • 为什么要有 JNI ?
    • 代码的可重用性
      1. 用 Java 重用现有的以及一些较老的代码(更多是用 C/C++ 编写的)。
    • 性能
      1. 解释模式下,本地代码的速度最快于 Java 20 倍左右。
      2. 现代的即使编译器(HotSpot)使其成为一个争议点。
    • 可以使 Java 调用一些底层的进程,如:O/S, H/W。
  • JNI 不可移植

📚 Tips

JNI 还可以用于本地编写的程序(如:C/C++)调用 Java 代码;
比如 Java 的命令行工具(Java 虚拟机启动 Java 代码)。

2.0 JNI 组件

  • javah 将包含 native 方法的 Java 类编译成 C 风格头文件的 JDK 工具
    将 Java 方法签名转化为本地函数原型。
  • jni.h JDK 包含的 C/C++ 头文件,用于将 Java 类型映射为本地对应的类型
    javah 会自动引入该文件到应用程序的头文件中。

3.0 JNI 开发 (Java)

  1. 创建一个包含 native 方法的 Java 类
    1
    public native void sayHi(String who, int times);
  2. 载入实现该方法的库
    1
    System.loadLibrary("HelloImpl");
  3. Java 调用本地方法

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.marakana.jniexamples;

public class Hello {
public native void sayHi(String who, int times); // 1
// 2
static {
System.loadLibrary("HelloImpl");
}
public static void main (String[] args) {
Hello hello = new Hello();
hello.sayHi(args[0], Integer.parseInt(args[1])); // 3
}
}

其中:
1、3 C/C++ 将实现 sayHi 方法,并且编译成库文件
2 库的名称:

  • Linux
    libHelloImpl.so
  • Windows
    HelloImpl.dll
  • macOS
    libHelloImpl.jnilib

注意:

Java 载入的库叫 HelloImpl

4.0 JNI 开发(C)

  • 我们用 JDK 提供的工具 javah 生成包含 sayHi 方法原型的头文件 package_name_classname.h。
    1. 编译生成 class 文件
      javac -d ./classes/ ./src/com/marakana/jniexamples/Hello.java
    2. 生成 com_marakana_jniexamples_Hello.h 头文件
      javah -jni com.marakana.jniexamples.Hello
  • 我们接着创建 com_marakana_jniexamples_Hello.c 来实现 Java_com_marakana_jniexamples_Hello_sayHi 函数。

com_marakana_jniexamples_Hello.h 文件如下:

1
2
3
4
5
#include <jni.h>

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi
(JNIEnv *, jobject, jstring, jint);

Hello.c 文件如下图:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include "com_marakana_jniexamples_Hello.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi(JNIEnv *env, jobject obj, jstring who, jint times) {
jint i;
jboolean iscopy;
const char *name;
name = (*env) -> GetStringUTFChars(env, who, &iscopy);
for (i = 0; i < times; i++) {
printf("Hello %s\n", name);
}
}

5.0 JNI 开发 (编译)

  • 我们接下来编译并且运行它(不同的系统会有不同的编译结果)。
  • 生成相应的库 libHelloImpl.so, HelloImpl.dll, libHelloImpl.jnilib。
  • 设置 LD_LIBRARY_PATH 为你库保存的路径。
  • 运行应用程序。

例如:为了编译类路径中的 com_marakana_jniexamples_Hello.c 文件(前提是你得确保 .h 以及 .c 文件在那)。

Linux

1
2
3
gcc -o libHelloImpl.so -lc -shared \
-I/usr/local/jdk1.6.0_03/include \
-I/usr/local/jdk1.6.0_03/include/linux com_marakana_jniexamples_Hello.c

macOS

1
2
gcc -o libHelloImpl.jnilib -lc -shared \
-I/System/Library/Frameworks/JavaVM.framework/Headers com_marakana_jniexamples_Hello.c

设置 LD_LIBRARY_PATH 环境变量。

1
export LD_LIBRARY_PATH=.

最后,运行你的应用程序。

1
2
3
4
5
6
java com.marakana.jniexamples.Hello Student 5
Hello Student
Hello Student
Hello Student
Hello Student
Hello Student

📚 Tips

比较常见的问题是 java.lang.UnsatisfiedLinkError 错误,而导致该错误的一般问题是共享库名称的错误、库并没有在指定的搜索路径上或者 Java 代码载入了错误的库。

6.0 类型转换

  • 一般情况,程序需要向本地方法传递参数以及接收本地方法的返回值。
  • Java 中存在两种类型:
    原始类型,如:int、float、char 等。
    引用类型,如:数组、字符串、实例、Classes 对象等。
  • 然而,原始类型与引用类型在 JNI 中有不同的处理方式。
    1. 在 JNI 中映射原始类型比较简单。

Table 3. JNI 数据类型映射

Java 类型 本地类型 描述
boolean jboolean 8 bits, unsigned
byte jbyte 8 bits, signed
char jchar 16 bits, unsigned
double jdouble 64 bits
float jfloat 32 bits
int jint 32 bits, signed
long jlong 64 bits, signed
short jshort 16 bits, signed
void void N/A
  1. 映射对象类型会更加复杂一点。这里我们主要关注字符串以及数组类型。不过不要着急,在我们深入探讨之前,先让我们看看本地方法的参数们。
  • JNI 把这些对象作为不透明引用传递给本地方法。
  • 不透明引用是 C 指针的一种类型,它指向 JVM 内部的数据结构

让我们考虑以下 Java class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.marakana.jniexamples;

public class HelloName {
public static native void sayHelloName(String name);

static {
System.loadLibrary("helloname");
}

public static void main (String[] args) {
HelloName hello = new HelloName();
String name = "John";
hello.sayHelloName(name);
}
}
  • .h 文件看起来如下所示:
1
2
3
4
#include <jni.h>

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName
(JNIEnv *, jclass, jstring);
  • 以下的 .c 文件并不会产生预期的结果:
    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    #include "com_marakana_jniexamples_HelloName.h"

    JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
    printf("Hello %s", name);
    }

    7.0 本地方法参数

  • 所有的本地方法实现都接收两个标准参数:
    1. JNIEnv *env: 指向函数表(指针数组)的指针的指针。函数表中每个条目都指向一个 JNI 函数,我们可以使用这些函数进行类型转换。
    2. 第二个参数依赖于本地方法是静态方法还是实例方法而不同。
      实例方法:它是一个 jobject 参数,该参数指向方法调用者对象。
      静态方法:它是一个 jclass 参数,该参数指向方法定义所在的 class 对象。

      8.0 字符串转换

  • 我们刚刚讨论的 JNIEnv *env 将会作为我们接下来找到的类型转换方法的参数来使用。
  • 有很多字符串相关的方法:
    • 一些方法将 java.lang.String 转化为 C 字符串,如:GetStringChars (Unicode format), GetStringUTFChars (UTF-8 format)
    • 一些方法转换 java.lang.String 为 C 字符串,如:NewString (Unicode format), NewStringUTF (UTF-8 format)
    • 一些方法用来释放 C 字符串内存,如:ReleaseStringChars, ReleaseStringUTFChars

📚 Tips

详细的内容可以参考:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/functions.html

  • 不知你是否还记得前一个例子,那是一个用来显示 “Hello name” 的本地方法:
1
2
3
4
5
6
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name) {
printf("Hello %s", name); // 1
}

因为 jstring 类型代表的是 Java 虚拟机中的字符串类型,而跟 C 中的字符串类型 (char *) 是不同的,所以这个例子不会按照预期运行。。

  • 以下是你需要做的,使用 UTF-8 string:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "com_marakana_jniexamples_HelloName.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass class, jstring name){
const jbyte *str;
str = (*env)->GetStringUTFChars(env, name, NULL); // 1
printf("Hello %s\n", str);
(*env)->ReleaseStringUTFChars(env, name, str); // 2
}

1 它返回一个指向代表 UTF-8 编码的字符串字节数组的指针(并没有产生内存复制)。
2 当我们并没有发生字符串复制的时候,调用 ReleaseStringUTFChars 函数可以防止字符串使用的内存区域保持固定状态。如果数据被复制,我们需要调用 ReleaseStringUTFChars 去释放那些不再使用的内存。

  • 这是另外一个例子,用于构造以及返回一个 java.lang.String 字符串实例:
  • 1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
    #include "com_marakana_jniexamples_GetName.h"

    JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) {
    char buffer[20];
    scanf("%s", buffer);
    return (*env)->NewStringUTF(env, buffer);
    }
  • 我们接下来将焦点放到原始数组上,因为它们与 JNI 中的对象数组不同。
  • 数组在 JNI 中由 jarray 引用类型及其“子类型”(例如 jintArray)表示。注意 jarray 并不是 C 数组!
  • 我们将要再一次使用 JNIEnv *env 参数访问类型转换方法
    • GetArrayRegion:复制原始数组的内容到预分配的 C 缓冲区中。当数组大小已知的情况下,该方法很好用。
    • GetArrayElements:获取指向原始数组的指针。
    • NewArray: 创建一个指定大小的数组。
  • 我们接下来看一个如何在本地环境中读取 Java 原始数组的例子。
  • 首先,看看 Java 程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.marakana.jniexamples;

public class ArrayReader {
private static native int sumArray(int[] arr); // 1
public static void main(String[] args) {
// Array declaration
int arr[] = new int[10];
// Fill the array
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
ArrayReader reader = new ArrayReader();
// Call native method
int result = reader.sumArray(arr); // 2
System.out.println("The sum of every element in the array is " + Integer.toString(result));
}
static {
System.loadLibrary("arrayreader");
}
}

1 2 这个方法将返回数组中元素的总和。

  • 运行 javah 之后,创建你的 .c 文件,如下图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include "com_marakana_jniexamples_ArrayReader.h"

JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_ArrayReader_sumArray(JNIEnv *env, jclass class, jintArray array) {
jint *native_array;
jint i, result = 0;
native_array = (*env) -> GetIntArrayElements(env, array, NULL); // 1
if (native_array == NULL) {
return 0;
}
for (i = 0; i < 10; i++) {
result += native_array[i];
}
(*env) -> ReleaseIntArrayElements(env, array, native_array, 0);
return result;
}

1 由于我们恰恰知道数组的大小,所以我们也可以使用 GetIntArrayRegion 函数

10 在本地世界中抛出异常

  • 我们将看到如何在本地世界抛出一个异常
  • 从本地世界抛出异常需要以下几个步骤:
    • 找到你想抛出异常的类
    • 抛出一个异常
    • 删除异常类的本地引用
  • 我们可以想象出这样一个实用函数:
1
2
3
4
5
6
7
void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) {
jclass class = (*env) -> FindClass(env, name); // 1
if (class != NULL) {
(*env) -> ThrowNew(env, class, message); // 2
}
(*env) -> DeleteLocalRef(env, class); // 3
}

1 通过名字找到该异常类
2 使用我们之前获得的类引用和异常信息抛出异常
3 删除异常类的本地引用

  • 以下是如何使用此程序的方法
1
ThrowExceptionByClassName(env,"java/lang/IllegalArgumentException","This exception is thrown from C code");

11 从本地代码访问属性和方法

  • 你可能想要通过调用本地代码来修改一些属性或者实例的调用方法
  • 总会是围绕这几个操作开始:通过调用 GetObjectClass 方法获取指向对象的引用。
  • 接着通过使用 GetFieldID 或者 GetMethodID 方法从 class 引用获取实例的字段 id 或者实例方法 id
  • 最后,不同的地方依赖于我们访问的是一个字段还是一个方法
  • 从这个 Java 类中,我们将会看到如何在本地代码中调用它的方法或者访问它的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.marakana.jniexamples;

public class InstanceAccess {
public String name; // 1

public void setName(String name) { // 2
this.name = name;
}

// Native method
public native void propertyAccess(); // 3
public native void methodAccess(); // 4

public static void main(String args[]) {
InstanceAccess instanceAccessor = new InstanceAccess();
// Set the initial value of the name property
instanceAccessor.setName("Jack");
System.out.println("Java: value of name = \""+ instanceAccessor.name +"\"");
// Call the propetyAccess() method
System.out.println("Java: calling propertyAccess() method...");
instanceAccessor.propertyAccess(); // 5
// Value of name after calling the propertyAccess() method
System.out.println("Java: value of name after calling propertyAccess() = \""+ instanceAccessor.name +"\"");
// Call the methodAccess() method
System.out.println("Java: calling methodAccess() method...");
instanceAccessor.methodAccess(); // 6
System.out.println("Java: value of name after calling methodAccess() = \""+ instanceAccessor.name +"\"");
}

// Load library
static {
System.loadLibrary("instanceaccess");
}
}

1 name 属性会在代码执行的时候被修改
2 该方法在本地代码修改 name 属性的时候被调用
3 5 本地方法通过直接访问 name 属性的方式对其进行修改
4 6 本地方法通过调用 Java setName() 方法对 name 属性进行修改

  • 以下就是我们用来本地执行的 C 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include "com_marakana_jniexamples_InstanceAccess.h"

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(JNIEnv *env, jobject object){
jfieldID fieldId;
jstring jstr;
const char *cString;

/* Getting a reference to object class */
jclass class = (*env) -> GetObjectClass(env, object); /* 1 */

/* Getting the field id in the class */
fieldId = (*env) -> GetFieldID(env, class, "name", "Ljava/lang/String;"); /* 2 */
if (fieldId == NULL) {
return; /* Error while getting field id */
}

/* Getting a jstring */
jstr = (*env) -> GetObjectField(env, object, fieldId); /* 3 */

/* From that jstring we are getting a C string: char* */
cString = (*env) -> GetStringUTFChars(env, jstr, NULL); /* 4 */
if (cString == NULL) {
return; /* Out of memory */
}
printf("C: value of name before property modification = \"%s\"\n", cString);
(*env) -> ReleaseStringUTFChars(env, jstr, cString);

/* Creating a new string containing the new name */
jstr = (*env) -> NewStringUTF(env, "Brian"); /* 5 */
if (jstr == NULL) {
return; /* Out of memory */
}
/* Overwrite the value of the name property */
(*env) -> SetObjectField(env, object, fieldId, jstr); /* 6 */
}

JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_methodAccess(JNIEnv *env, jobject object){
jclass class = (*env) -> GetObjectClass(env, object); /* 7 */
jmethodID methodId = (*env) -> GetMethodID(env, class, "setName", "(Ljava/lang/String;)V"); /* 8 */
jstring jstr;
if (methodId == NULL) {
return; /* method not found */
}
/* Creating a new string containing the new name */
jstr = (*env) -> NewStringUTF(env, "Nick"); /* 9 */
(*env) -> CallVoidMethod(env, object, methodId, jstr); /* 10 */
}

1 7 获取 class 对象的引用
2 从 class 对象中获取字段 Id,以及指定要获取的属性以及内部类型。可以从以下链接中获取关于 jni 类型的信息:http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html
3 这里将会返回本地类型中的属性值 jstring
4 我们需要将 jstring 类型转换为 C 中的字符串
5 这里会创建出一个新的 java.lang.String 类型用以修改属性的值
6 将新的值设置给该属性
8 从先前获取到的 class 对象中通过方法的名称以及签名获取方法 id 。这里有一个用来获取方法签名的实用工具:javap -s -p ClassName for instance javap -s -p InstanceAccess
9 创建出一个新的 java.lang.String 对象作为从本地代码调用 java 方法的参数。
10 由于 Java 方法返回值类型为 void,所以调用 CallVoidMethod 方法,并且将先前创建出的 jstring 作为参数传递给它


参考